/*

 Audiometria
 Version 0.1
 
 Audiometry: hearing test, equal loudness contours (hearing thresholds), 
 sensitivity and acuity.
 
 Warning 1:
 Start with the master sound low!
    Loud sounds can hurt or damage your ears.
    I do not give any warranty nor take any responsibility about any bad usage
        of this software.
 
 
 Warning 2:
 This test does not, in any manner, substitute a doctor!
    This software is for informational purposes only.
    You can use to compare your left and right ears or to equalize your computer.
    The spectral purity and the sound level depend on your sound card and 
        headphones.
    Daily fluctuations in hearing are common.
    If you are worried about your listening, consult an audiologist.

 
 Copyright © 2012 Marcos Assis
 (except when mentioned)
 
 This program is free software: you can redistribute it and/or modify it under 
 the terms of the GNU General Public License as published by the Free Software 
 Foundation, either version 3 of the License, or (at your option) any later
 version. 
 
 This program is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 PARTICULAR PURPOSE.  See the GNU General Public License for more details. 
 
 You should have received a copy of the GNU General Public License along with
 this program.  If not, see http://www.gnu.org/licenses.

*/
package audiometria;

import audiometria.Audiometry.SoundSample;
import java.awt.Desktop;
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.*;
import javax.swing.DefaultListModel;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.Timer;
import javax.swing.filechooser.FileNameExtensionFilter;
import util.JFileChooserWithConfirmation;

/**
 * Main frame for the Audiometria software.
 *
 * @author marcos
 */
public class MainFrame extends javax.swing.JFrame {

    /**
     * Return type when querying for the user choice on reset or close events.
     */
    public static enum SaveChoice {

        CANCELED, SAVED, PROCEED_WITHOUT_SAVE
    }

    /**
     * Creates new form MainFrame
     */
    public MainFrame() {
        initComponents();

        jButtonPlayTestSound.requestFocus();

        KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
        kfm.addKeyEventDispatcher(new KeyEventDispatcher() {

            @Override
            public boolean dispatchKeyEvent(KeyEvent e) {
                if (e.getID() == KeyEvent.KEY_PRESSED) {
                    if (e.getKeyCode() == KeyEvent.VK_LEFT) {
                        jButtonLeftEar.doClick();
                    } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
                        jButtonRightEar.doClick();
                    } else if (e.getKeyCode() == KeyEvent.VK_SPACE) {
                        jToggleButtonStartPause.doClick();
                    }
                }
                return false;
            }
        ;
    }

    );
    }

    /**
     * Queries the default clip player from Java Sound API and set it as the
     * clip player for the class.
     */
    protected void setDefaultClipPlayer() {
        clip = null;
        try {
            clip = AudioSystem.getClip();
        } catch (LineUnavailableException ex) {
            Logger.getLogger(MainFrame.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Play a sinusoidal wave.
     * 
     * @param frequency
     * @param dB
     * @param duration
     * @param channels 
     */
    protected void playSinus(float frequency, float dB, float duration, SinusGenerator.Channels channels) {
        try {
            if (clip == null || clip.isActive()) {
                return;
            }
            float amp = Audiometry.dB2lin(dB) * jSliderMasterLevel.getValue() / 100;
            clip.close();
            clip.open(SinusGenerator.generateSinus(amp, frequency, duration, channels));
            clip.start();
        } catch (LineUnavailableException | IOException ex) {
            Logger.getLogger(MainFrame.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        jDialogChooseAudioOutput = new javax.swing.JDialog();
        jLabel1 = new javax.swing.JLabel();
        jScrollPane1 = new javax.swing.JScrollPane();
        jListMixers = new javax.swing.JList<Mixer.Info>();
        jLabel2 = new javax.swing.JLabel();
        jScrollPane2 = new javax.swing.JScrollPane();
        jListLines = new javax.swing.JList<Line.Info>();
        jLabelDialogStatusBar = new javax.swing.JLabel();
        jPanel1 = new javax.swing.JPanel();
        jTextFieldFrequency = new javax.swing.JTextField();
        jLabel3 = new javax.swing.JLabel();
        jLabel5 = new javax.swing.JLabel();
        jTextFieldAmplitude = new javax.swing.JTextField();
        jLabel4 = new javax.swing.JLabel();
        jTextFieldTestSoundDuration = new javax.swing.JTextField();
        jButtonPlayTestSound = new javax.swing.JButton();
        jCheckBoxLeftOn = new javax.swing.JCheckBox();
        jCheckBoxRightOn = new javax.swing.JCheckBox();
        jSliderMasterLevel = new javax.swing.JSlider();
        jPanel2 = new javax.swing.JPanel();
        jScrollPane3 = new javax.swing.JScrollPane();
        jTableExamresults = new javax.swing.JTable();
        jPanel3 = new javax.swing.JPanel();
        jButtonLeftEar = new javax.swing.JButton();
        jLabel8 = new javax.swing.JLabel();
        jLabel9 = new javax.swing.JLabel();
        jButtonRightEar = new javax.swing.JButton();
        jPanel4 = new javax.swing.JPanel();
        jTextFieldTimeBetweenSounds = new javax.swing.JTextField();
        jLabel7 = new javax.swing.JLabel();
        jLabel6 = new javax.swing.JLabel();
        jTextFieldSoundDuration = new javax.swing.JTextField();
        jPanel5 = new javax.swing.JPanel();
        jToggleButtonStartPause = new javax.swing.JToggleButton();
        jButtonSave = new javax.swing.JButton();
        jButtonReset = new javax.swing.JButton();
        jScrollPane4 = new javax.swing.JScrollPane();
        jTextArea1 = new javax.swing.JTextArea();
        jCheckBoxShowSoundProperties = new javax.swing.JCheckBox();
        jLabelLegend = new javax.swing.JLabel();
        jMenuBar1 = new javax.swing.JMenuBar();
        jMenuFile = new javax.swing.JMenu();
        jMenuItemSelectAudioOutput = new javax.swing.JMenuItem();
        jMenuItemSave = new javax.swing.JMenuItem();
        jSeparator2 = new javax.swing.JPopupMenu.Separator();
        jMenuItemExit = new javax.swing.JMenuItem();
        jMenuHelp = new javax.swing.JMenu();
        jMenuItemHelp = new javax.swing.JMenuItem();
        jMenuItemAbout = new javax.swing.JMenuItem();

        jDialogChooseAudioOutput.setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
        jDialogChooseAudioOutput.setTitle("Choose audio output");
        jDialogChooseAudioOutput.addWindowListener(new java.awt.event.WindowAdapter() {
            public void windowClosed(java.awt.event.WindowEvent evt) {
                jDialogChooseAudioOutputWindowClosed(evt);
            }
        });

        jLabel1.setText("Choose mixer");

        jListMixers.setModel(new javax.swing.AbstractListModel() {
            String[] strings = { "Avalaible mixers" };
            public int getSize() { return strings.length; }
            public Object getElementAt(int i) { return strings[i]; }
        });
        jListMixers.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
        jListMixers.addListSelectionListener(new javax.swing.event.ListSelectionListener() {
            public void valueChanged(javax.swing.event.ListSelectionEvent evt) {
                jListMixersValueChanged(evt);
            }
        });
        jScrollPane1.setViewportView(jListMixers);

        jLabel2.setText("Choose line");

        jListLines.setModel(new javax.swing.AbstractListModel() {
            String[] strings = { "Avalaible lines" };
            public int getSize() { return strings.length; }
            public Object getElementAt(int i) { return strings[i]; }
        });
        jListLines.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
        jListLines.addListSelectionListener(new javax.swing.event.ListSelectionListener() {
            public void valueChanged(javax.swing.event.ListSelectionEvent evt) {
                jListLinesValueChanged(evt);
            }
        });
        jScrollPane2.setViewportView(jListLines);

        jLabelDialogStatusBar.setText("<html>Mixer:<br>Line:</html>");

        javax.swing.GroupLayout jDialogChooseAudioOutputLayout = new javax.swing.GroupLayout(jDialogChooseAudioOutput.getContentPane());
        jDialogChooseAudioOutput.getContentPane().setLayout(jDialogChooseAudioOutputLayout);
        jDialogChooseAudioOutputLayout.setHorizontalGroup(
            jDialogChooseAudioOutputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jDialogChooseAudioOutputLayout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jDialogChooseAudioOutputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jScrollPane2, javax.swing.GroupLayout.Alignment.TRAILING)
                    .addComponent(jScrollPane1)
                    .addGroup(jDialogChooseAudioOutputLayout.createSequentialGroup()
                        .addGroup(jDialogChooseAudioOutputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addComponent(jLabel1)
                            .addComponent(jLabel2))
                        .addGap(0, 0, Short.MAX_VALUE))
                    .addComponent(jLabelDialogStatusBar))
                .addContainerGap())
        );
        jDialogChooseAudioOutputLayout.setVerticalGroup(
            jDialogChooseAudioOutputLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jDialogChooseAudioOutputLayout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jLabel1)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jScrollPane1)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(jLabel2)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jScrollPane2)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(jLabelDialogStatusBar, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap())
        );

        setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE);
        setTitle("Audiometria");
        addWindowListener(new java.awt.event.WindowAdapter() {
            public void windowClosing(java.awt.event.WindowEvent evt) {
                formWindowClosing(evt);
            }
            public void windowDeactivated(java.awt.event.WindowEvent evt) {
                formWindowDeactivated(evt);
            }
            public void windowClosed(java.awt.event.WindowEvent evt) {
                formWindowClosed(evt);
            }
            public void windowOpened(java.awt.event.WindowEvent evt) {
                formWindowOpened(evt);
            }
        });

        jPanel1.setBorder(javax.swing.BorderFactory.createTitledBorder("Test sound"));

        jTextFieldFrequency.setText("1000");

        jLabel3.setText("Frequency [Hz]");

        jLabel5.setText("Amplitude [dB]");
        jLabel5.setToolTipText("Volume of the test sound in percents");

        jTextFieldAmplitude.setText("-12");

        jLabel4.setText("Duration [s]");

        jTextFieldTestSoundDuration.setText("0.5");

        jButtonPlayTestSound.setText("Play");
        jButtonPlayTestSound.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButtonPlayTestSoundActionPerformed(evt);
            }
        });

        jCheckBoxLeftOn.setSelected(true);
        jCheckBoxLeftOn.setText("Left on");

        jCheckBoxRightOn.setSelected(true);
        jCheckBoxRightOn.setText("Right on");

        javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1);
        jPanel1.setLayout(jPanel1Layout);
        jPanel1Layout.setHorizontalGroup(
            jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel1Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
                    .addComponent(jLabel3, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(jTextFieldFrequency, javax.swing.GroupLayout.PREFERRED_SIZE, 74, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
                    .addComponent(jLabel4, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(jTextFieldTestSoundDuration, javax.swing.GroupLayout.PREFERRED_SIZE, 57, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
                    .addComponent(jLabel5, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(jTextFieldAmplitude, javax.swing.GroupLayout.PREFERRED_SIZE, 69, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addGap(34, 34, 34)
                .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jCheckBoxLeftOn)
                    .addGroup(jPanel1Layout.createSequentialGroup()
                        .addComponent(jCheckBoxRightOn)
                        .addGap(18, 18, 18)
                        .addComponent(jButtonPlayTestSound)))
                .addContainerGap(312, Short.MAX_VALUE))
        );
        jPanel1Layout.setVerticalGroup(
            jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel1Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jLabel3)
                    .addComponent(jLabel4)
                    .addComponent(jLabel5)
                    .addComponent(jCheckBoxLeftOn))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jTextFieldFrequency, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(jTextFieldTestSoundDuration, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(jTextFieldAmplitude, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(jCheckBoxRightOn)
                    .addComponent(jButtonPlayTestSound))
                .addContainerGap(16, Short.MAX_VALUE))
        );

        jSliderMasterLevel.setMajorTickSpacing(10);
        jSliderMasterLevel.setMinorTickSpacing(1);
        jSliderMasterLevel.setOrientation(javax.swing.JSlider.VERTICAL);
        jSliderMasterLevel.setPaintLabels(true);
        jSliderMasterLevel.setPaintTicks(true);
        jSliderMasterLevel.setToolTipText("This limit will be treated like the 100% level in the exam");
        jSliderMasterLevel.setBorder(javax.swing.BorderFactory.createTitledBorder("Master level [%]"));

        jPanel2.setBorder(javax.swing.BorderFactory.createTitledBorder("Exam"));
        jPanel2.setToolTipText("");

        jTableExamresults.setModel(new javax.swing.table.DefaultTableModel(
            new Object [][] {
                { new Integer(0), null, null, null, null, null, null},
                { new Integer(-6), null, null, null, null, null, null},
                { new Integer(-12), null, null, null, null, null, null},
                { new Integer(-18), null, null, null, null, null, null},
                { new Integer(-24), null, null, null, null, null, null},
                { new Integer(-30), null, null, null, null, null, null},
                { new Integer(-36), null, null, null, null, null, null},
                { new Integer(-42), null, null, null, null, null, null},
                { new Integer(-48), null, null, null, null, null, null},
                { new Integer(-54), null, null, null, null, null, null},
                { new Integer(-60), "", "", "", "", "", ""}
            },
            new String [] {
                "Amplitudes [%]", "250 Hz", "500 Hz", "1000 Hz", "2000 Hz", "4000 Hz", "8000 Hz"
            }
        ) {
            Class[] types = new Class [] {
                java.lang.Integer.class, java.lang.String.class, java.lang.String.class, java.lang.String.class, java.lang.String.class, java.lang.String.class, java.lang.String.class
            };
            boolean[] canEdit = new boolean [] {
                false, false, false, false, false, false, false
            };

            public Class getColumnClass(int columnIndex) {
                return types [columnIndex];
            }

            public boolean isCellEditable(int rowIndex, int columnIndex) {
                return canEdit [columnIndex];
            }
        });
        jTableExamresults.setToolTipText("");
        jTableExamresults.setColumnSelectionAllowed(true);
        jTableExamresults.getTableHeader().setReorderingAllowed(false);
        jScrollPane3.setViewportView(jTableExamresults);
        jTableExamresults.getColumnModel().getSelectionModel().setSelectionMode(javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);

        jPanel3.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jButtonLeftEar.setText("I can hear it!");
        jButtonLeftEar.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButtonLeftEarActionPerformed(evt);
            }
        });

        jLabel8.setText("Left ear (left arrow)");

        jLabel9.setText("Right ear (right arrow)");

        jButtonRightEar.setText("I can hear it!");
        jButtonRightEar.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButtonRightEarActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout jPanel3Layout = new javax.swing.GroupLayout(jPanel3);
        jPanel3.setLayout(jPanel3Layout);
        jPanel3Layout.setHorizontalGroup(
            jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel3Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jButtonLeftEar)
                    .addComponent(jLabel8))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jLabel9)
                    .addComponent(jButtonRightEar))
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        );
        jPanel3Layout.setVerticalGroup(
            jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel3Layout.createSequentialGroup()
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
                    .addGroup(jPanel3Layout.createSequentialGroup()
                        .addComponent(jLabel9)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(jButtonRightEar))
                    .addGroup(jPanel3Layout.createSequentialGroup()
                        .addComponent(jLabel8)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(jButtonLeftEar)))
                .addContainerGap())
        );

        jPanel4.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        jTextFieldTimeBetweenSounds.setText("0.5");
        jTextFieldTimeBetweenSounds.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                timerPeriodChanged(evt);
            }
        });

        jLabel7.setText("Time between sounds [s]");

        jLabel6.setText("Sound duration [s]");

        jTextFieldSoundDuration.setText("0.5");
        jTextFieldSoundDuration.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                timerPeriodChanged(evt);
            }
        });

        javax.swing.GroupLayout jPanel4Layout = new javax.swing.GroupLayout(jPanel4);
        jPanel4.setLayout(jPanel4Layout);
        jPanel4Layout.setHorizontalGroup(
            jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel4Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jTextFieldSoundDuration, javax.swing.GroupLayout.PREFERRED_SIZE, 89, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(jLabel6))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
                    .addComponent(jLabel7, javax.swing.GroupLayout.DEFAULT_SIZE, 124, Short.MAX_VALUE)
                    .addComponent(jTextFieldTimeBetweenSounds))
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        );
        jPanel4Layout.setVerticalGroup(
            jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel4Layout.createSequentialGroup()
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                .addGroup(jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jLabel7)
                    .addComponent(jLabel6))
                .addGroup(jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jTextFieldSoundDuration, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(jTextFieldTimeBetweenSounds, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addGap(14, 14, 14))
        );

        jToggleButtonStartPause.setText("Start / Pause");
        jToggleButtonStartPause.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jToggleButtonStartPauseActionPerformed(evt);
            }
        });

        jButtonSave.setText("Save");
        jButtonSave.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButtonSaveActionPerformed(evt);
            }
        });

        jButtonReset.setText("Reset");
        jButtonReset.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButtonResetActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout jPanel5Layout = new javax.swing.GroupLayout(jPanel5);
        jPanel5.setLayout(jPanel5Layout);
        jPanel5Layout.setHorizontalGroup(
            jPanel5Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel5Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel5Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
                    .addComponent(jToggleButtonStartPause, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(jButtonSave, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(jButtonReset, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        );
        jPanel5Layout.setVerticalGroup(
            jPanel5Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel5Layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jToggleButtonStartPause)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                .addComponent(jButtonReset)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(jButtonSave)
                .addContainerGap())
        );

        jTextArea1.setColumns(20);
        jTextArea1.setRows(5);
        jScrollPane4.setViewportView(jTextArea1);

        jCheckBoxShowSoundProperties.setSelected(true);
        jCheckBoxShowSoundProperties.setText("Show sound properties here");

        jLabelLegend.setText("<html> The table shows the <br /> minimium amplitude <br /> <strong>heard</strong> sounds. <br /> <strong>x</strong>'s stands for the <strong>left</strong> ear <br /> <strong>o</strong>'s stands for the <strong>right</strong> ear <br /> </html>");

        javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2);
        jPanel2.setLayout(jPanel2Layout);
        jPanel2Layout.setHorizontalGroup(
            jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel2Layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jScrollPane3)
                    .addGroup(jPanel2Layout.createSequentialGroup()
                        .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addComponent(jScrollPane4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addComponent(jCheckBoxShowSoundProperties))
                        .addGap(18, 18, 18)
                        .addComponent(jPanel5, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 10, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
                            .addComponent(jPanel4, javax.swing.GroupLayout.PREFERRED_SIZE, 238, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addGroup(jPanel2Layout.createSequentialGroup()
                                .addComponent(jLabelLegend, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 12, Short.MAX_VALUE)
                                .addComponent(jPanel3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))))
                .addContainerGap())
        );
        jPanel2Layout.setVerticalGroup(
            jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel2Layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jScrollPane3, javax.swing.GroupLayout.PREFERRED_SIZE, 203, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jPanel5, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addGroup(jPanel2Layout.createSequentialGroup()
                        .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
                            .addGroup(jPanel2Layout.createSequentialGroup()
                                .addComponent(jCheckBoxShowSoundProperties)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(jScrollPane4))
                            .addGroup(jPanel2Layout.createSequentialGroup()
                                .addComponent(jPanel4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                                .addComponent(jPanel3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
                        .addGap(0, 0, Short.MAX_VALUE))
                    .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel2Layout.createSequentialGroup()
                        .addGap(0, 0, Short.MAX_VALUE)
                        .addComponent(jLabelLegend, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
                .addContainerGap())
        );

        jMenuFile.setText("File");

        jMenuItemSelectAudioOutput.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_O, java.awt.event.InputEvent.CTRL_MASK));
        jMenuItemSelectAudioOutput.setText("Select audio output");
        jMenuItemSelectAudioOutput.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemSelectAudioOutputActionPerformed(evt);
            }
        });
        jMenuFile.add(jMenuItemSelectAudioOutput);

        jMenuItemSave.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_S, java.awt.event.InputEvent.CTRL_MASK));
        jMenuItemSave.setText("Save");
        jMenuItemSave.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemSaveActionPerformed(evt);
            }
        });
        jMenuFile.add(jMenuItemSave);
        jMenuFile.add(jSeparator2);

        jMenuItemExit.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_W, java.awt.event.InputEvent.CTRL_MASK));
        jMenuItemExit.setText("Exit");
        jMenuItemExit.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemExitActionPerformed(evt);
            }
        });
        jMenuFile.add(jMenuItemExit);

        jMenuBar1.add(jMenuFile);

        jMenuHelp.setText("Help");

        jMenuItemHelp.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_F1, 0));
        jMenuItemHelp.setText("Help");
        jMenuItemHelp.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemHelpActionPerformed(evt);
            }
        });
        jMenuHelp.add(jMenuItemHelp);

        jMenuItemAbout.setText("About");
        jMenuItemAbout.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItemAboutActionPerformed(evt);
            }
        });
        jMenuHelp.add(jMenuItemAbout);

        jMenuBar1.add(jMenuHelp);

        setJMenuBar(jMenuBar1);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(jPanel2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(jSliderMasterLevel, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jSliderMasterLevel, javax.swing.GroupLayout.DEFAULT_SIZE, 520, Short.MAX_VALUE)
                    .addGroup(layout.createSequentialGroup()
                        .addComponent(jPanel2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                        .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
                .addContainerGap())
        );

        pack();
    }// </editor-fold>//GEN-END:initComponents

    private void jMenuItemSelectAudioOutputActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemSelectAudioOutputActionPerformed
        mixerInfo = AudioSystem.getMixerInfo();

        DefaultListModel<Mixer.Info> model = new DefaultListModel<>();
        for (Mixer.Info mi : mixerInfo) {
            model.addElement(mi);
        }

        jListMixers.setModel(model);
        jDialogChooseAudioOutput.setVisible(true);
        jDialogChooseAudioOutput.pack();
    }//GEN-LAST:event_jMenuItemSelectAudioOutputActionPerformed

    private void jMenuItemExitActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemExitActionPerformed
        if (askToSave() != SaveChoice.CANCELED) {
            this.dispose();
        }
    }//GEN-LAST:event_jMenuItemExitActionPerformed

    private void jListMixersValueChanged(javax.swing.event.ListSelectionEvent evt) {//GEN-FIRST:event_jListMixersValueChanged
        if (evt.getValueIsAdjusting() || jListMixers.getSelectedIndex() < 0) {
            return;
        }

        clip = null;
        if (mixer != null) {
            mixer.close();
        }

        mixer = AudioSystem.getMixer(mixerInfo[jListMixers.getSelectedIndex()]);
        Line.Info[] targetSourceInfo = mixer.getSourceLineInfo();

        try {
            mixer.open();
        } catch (LineUnavailableException ex) {
            Logger.getLogger(MainFrame.class.getName()).log(Level.SEVERE, null, ex);
        }

        DefaultListModel<Line.Info> model = new DefaultListModel<>();
        for (Line.Info li : targetSourceInfo) {
            model.addElement(li);
        }

        jListLines.setModel(model);

        jDialogChooseAudioOutput.pack();
        jLabelDialogStatusBar.setText(String.format("<html>Mixer: %s<br>Line: null</html>", mixer.getMixerInfo()));
    }//GEN-LAST:event_jListMixersValueChanged

    private void jListLinesValueChanged(javax.swing.event.ListSelectionEvent evt) {//GEN-FIRST:event_jListLinesValueChanged
        if (evt.getValueIsAdjusting() || jListMixers.getSelectedIndex() < 0 || jListLines.getSelectedIndex() < 0) {
            return;
        }

        if (jListLines.getSelectedValue() != null) {
            try {
                if (clip != null) {
                    clip.close();
                }
                clip = (Clip) mixer.getLine(jListLines.getSelectedValue());

                jLabelDialogStatusBar.setText(String.format("<html>Mixer: %s<br>Line open? %b %s</html>",
                        mixer.getMixerInfo(), clip.isOpen(), clip.getLineInfo()));

            } catch (ClassCastException ex) {
                clip = null;
                jLabelDialogStatusBar.setText(String.format("<html>Mixer: %s<br>Line: null</html>", mixer.getMixerInfo()));
            } catch (LineUnavailableException ex) {
                jLabelDialogStatusBar.setText("Couldn't open line.");
                Logger.getLogger(MainFrame.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        jDialogChooseAudioOutput.pack();
    }//GEN-LAST:event_jListLinesValueChanged

    private void jButtonPlayTestSoundActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButtonPlayTestSoundActionPerformed

        float frequency = Float.parseFloat(jTextFieldFrequency.getText());
        float duration = Float.parseFloat(jTextFieldTestSoundDuration.getText());
        float amplitude = Float.parseFloat(jTextFieldAmplitude.getText());
        playSinus(frequency, amplitude, duration,
                SinusGenerator.Channels.getChannels(jCheckBoxLeftOn.isSelected(), jCheckBoxRightOn.isSelected()));

    }//GEN-LAST:event_jButtonPlayTestSoundActionPerformed

    private void jDialogChooseAudioOutputWindowClosed(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_jDialogChooseAudioOutputWindowClosed
        if (clip == null) {
            setDefaultClipPlayer();
        }
    }//GEN-LAST:event_jDialogChooseAudioOutputWindowClosed

    private void formWindowOpened(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowOpened
        audiometry = new Audiometry();
        setDefaultClipPlayer();

        int period = (int) (1000 * (Float.parseFloat(jTextFieldSoundDuration.getText())
                + Float.parseFloat(jTextFieldTimeBetweenSounds.getText())));

        timerSoundSample = new Timer(period, new java.awt.event.ActionListener() {

            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                timerSoundSampleActionPerformed(evt);
            }
        });
    }//GEN-LAST:event_formWindowOpened

    private void jButtonLeftEarActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButtonLeftEarActionPerformed
        if (soundSample != null && timerSoundSample.isRunning() && !heardLastSound
                && soundSample.isLeftChannel) {
            audiometry.giveFeedback(soundSample, true);
            heardLastSound = true;
        }
    }//GEN-LAST:event_jButtonLeftEarActionPerformed

    private void jButtonRightEarActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButtonRightEarActionPerformed
        if (soundSample != null && timerSoundSample.isRunning() && !heardLastSound
                && !soundSample.isLeftChannel) {
            audiometry.giveFeedback(soundSample, true);
            heardLastSound = true;
        }
    }//GEN-LAST:event_jButtonRightEarActionPerformed

    private void timerPeriodChanged(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_timerPeriodChanged
        int period = (int) (1000 * (Float.parseFloat(jTextFieldSoundDuration.getText())
                + Float.parseFloat(jTextFieldTimeBetweenSounds.getText())));
        timerSoundSample.setDelay(period);
    }//GEN-LAST:event_timerPeriodChanged

    private void jToggleButtonStartPauseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jToggleButtonStartPauseActionPerformed
        timerPeriodChanged(evt);
        if (jToggleButtonStartPause.isSelected()) {
            timerSoundSample.start();
            updateText();
        } else {
            timerSoundSample.stop();
            updateText();
        }
    }//GEN-LAST:event_jToggleButtonStartPauseActionPerformed

    private void formWindowClosed(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosed
        timerSoundSample.stop();
    }//GEN-LAST:event_formWindowClosed

    private void formWindowDeactivated(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowDeactivated
        pause();
    }//GEN-LAST:event_formWindowDeactivated

    private void jButtonResetActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButtonResetActionPerformed
        pause();
        if (askToSave() == SaveChoice.CANCELED) {
            return;
        }
        reset();
    }//GEN-LAST:event_jButtonResetActionPerformed

    private void jButtonSaveActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButtonSaveActionPerformed
        save();
    }//GEN-LAST:event_jButtonSaveActionPerformed

    private void formWindowClosing(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosing
        if (askToSave() != SaveChoice.CANCELED) {
            this.dispose();
        }
    }//GEN-LAST:event_formWindowClosing

    private void jMenuItemSaveActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemSaveActionPerformed
        save();
    }//GEN-LAST:event_jMenuItemSaveActionPerformed

    private void jMenuItemHelpActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemHelpActionPerformed
        JOptionPane.showMessageDialog(this, "Sorry, help is under construction!\n\n"
                + "See About Audiometria.htm.\nCopyright © 2012 Marcos Assis");

        browseAbout();
    }//GEN-LAST:event_jMenuItemHelpActionPerformed

    private void jMenuItemAboutActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItemAboutActionPerformed
        JOptionPane.showMessageDialog(this,
                "See About Audiometria.htm.\nCopyright © 2012 Marcos Assis");

        browseAbout();
    }//GEN-LAST:event_jMenuItemAboutActionPerformed

    private void timerSoundSampleActionPerformed(java.awt.event.ActionEvent evt) {
        if (soundSample != null && samplesCount > 0 && !heardLastSound) {
            audiometry.giveFeedback(soundSample, false);
        }

        ++samplesCount;
        heardLastSound = false;

        soundSample = audiometry.getSoundSample();
        updateText();
        if (soundSample == null) {
            pause();
            showResults();
        } else {

            playSinus(soundSample.frequency, soundSample.amplitude,
                    Float.parseFloat(jTextFieldSoundDuration.getText()),
                    SinusGenerator.Channels.getChannels(soundSample.isLeftChannel, !soundSample.isLeftChannel));
        }
    }

    private void pause() {
        if (jToggleButtonStartPause.isSelected()) {
            jToggleButtonStartPause.doClick();
        }
    }

    /**
     * Show results of an hearing threshold audiometry test in the table.
     */
    protected void showResults() {
        Float a;
        for (float f : audiometry.getFrequencies()) {
            a = audiometry.getLeftEarThreshold().get(f);
            if (a != null) {
                showPointOnTable(f, a, true);
            }
            a = audiometry.getRightEarThreshold().get(f);
            if (a != null) {
                showPointOnTable(f, a, false);
            }
        }
    }

    /**
     * Show a point of the hearing threshold in the table.
     * 
     * @param f
     * @param a
     * @param isLeft 
     */
    protected void showPointOnTable(Float f, Float a, boolean isLeft) {
        int column;
        String fsrt = String.format("%d", Math.round(f));
        for (column = 1; column < jTableExamresults.getColumnCount(); ++column) {
            if (jTableExamresults.getColumnName(column).startsWith(fsrt)) {
                break;
            }
        }

        int row;
        for (row = 0; row < jTableExamresults.getRowCount(); ++row) {
            Integer a1 = (Integer) jTableExamresults.getValueAt(row, 0);
            if (a1.equals((int) a.floatValue())) {
                break;
            }
        }

        if (row >= jTableExamresults.getRowCount() || column >= jTableExamresults.getColumnCount()) {
            throw new RuntimeException(String.format("Could not update table: %d %d", row, column));
        }

        String text = (String) jTableExamresults.getValueAt(row, column);
        if (text == null) {
            text = "";
        }
        if (isLeft) {
            text += "x";
        } else {
            text += "o";
        }

        jTableExamresults.setValueAt(text, row, column);
    }

    /**
     * Update the text about the actual sample.
     */
    private void updateText() {
        String text;
        if (timerSoundSample.isRunning()) {
            text = "Playing";
        } else {
            text = "Stopped";
        }
        text += "\nSample n° " + samplesCount;
        if (soundSample != null && jCheckBoxShowSoundProperties.isSelected()) {

            text += "\nFrequency: " + soundSample.frequency + " Hz"
                    + "\nAmplitude: " + soundSample.amplitude + " dB";

            if (soundSample.isLeftChannel) {
                text += "\nLeft side";
            } else {
                text += "\nRight side";
            }
        }
        jTextArea1.setText(text);
    }

    /**
     * Ask user's confirmation for exit/reset and ask to save.
     * 
     * @return User choice
     */
    protected SaveChoice askToSave() {

        if (saved || audiometry == null
                || (audiometry.getLeftEarThreshold().isEmpty() && audiometry.getRightEarThreshold().isEmpty())) {
            return SaveChoice.PROCEED_WITHOUT_SAVE;
        }

        Object[] options = {"Save before", "Proceed without save", "Cancel"};
        int dialogReturn = JOptionPane.showOptionDialog(this, "Are you sure?", "Confirmation",
                JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
                null, options, options[2]);

        if (dialogReturn == 0) {
            return save();
        }
        if (dialogReturn == 1) {
            return SaveChoice.PROCEED_WITHOUT_SAVE;
        }
        return SaveChoice.CANCELED;
    }

    /**
     * Saves a simple text file with the audiometry results.
     * 
     * @return Or the file was saved, or user canceled (reset/exit), or there 
     * was nothing to save.
     */
    protected SaveChoice save() {
        if (audiometry.getLeftEarThreshold().isEmpty() && audiometry.getRightEarThreshold().isEmpty()) {
            return SaveChoice.PROCEED_WITHOUT_SAVE;
        }

        JFileChooser jFileChooser = new JFileChooserWithConfirmation();
        jFileChooser.setFileFilter(new FileNameExtensionFilter("Text files", "txt"));

        int dialogReturn = jFileChooser.showSaveDialog(this);
        if (dialogReturn != JFileChooser.APPROVE_OPTION) {
            return SaveChoice.CANCELED;
        }

        FileWriter out = null;
        try {
            out = new FileWriter(jFileChooser.getSelectedFile());
            out.write(String.format("frequency [Hz]\tleft\tright (minimal amplitude heard in dB)\r\n"));
            String text;
            Float a;
            for (float f : audiometry.getFrequencies()) {
                text = (new Float(f)).toString() + "\t\t";
                a = audiometry.getLeftEarThreshold().get(f);
                if (a == null) {
                    text += "---\t";
                } else {
                    text += a.toString() + "\t";
                }
                a = audiometry.getRightEarThreshold().get(f);
                if (a == null) {
                    text += "---\r\n";
                } else {
                    text += a.toString() + "\r\n";
                }

                out.write(String.format(text));
            }
            saved = true;
        } catch (IOException ex) {
            Logger.getLogger(MainFrame.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            try {
                out.close();
            } catch (IOException ex) {
                Logger.getLogger(MainFrame.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

        return SaveChoice.SAVED;
    }

    /**
     * Resets enviroment for a new test.
     */
    protected void reset() {
        soundSample = null;
        samplesCount = 0;
        saved = false;
        audiometry = new Audiometry();
        for (int row = 0; row < jTableExamresults.getRowCount(); ++row) {
            for (int column = 1; column < jTableExamresults.getColumnCount(); ++column) {
                jTableExamresults.setValueAt(null, row, column);
            }
        }
    }

    /**
     * Opens About file in the default browser.
     */
    protected void browseAbout() {
        File file = new File("About Audiometria.htm");
        if (file.exists() && Desktop.isDesktopSupported()) {
            try {
                Desktop.getDesktop().browse(file.toURI());
            } catch (IOException ex) {
                Logger.getLogger(MainFrame.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) {
        // TODO user choose frequencies and amplitudes
        // TODO block multiple print results on the table
        // TODO translate to portuguese
        // TODO translate to esperanto
        // TODO help and initialization dialog

        /*
         * Set the Nimbus look and feel
         */
        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
        /*
         * If Nimbus (introduced in Java SE 6) is not available, stay with the
         * default look and feel. For details see
         * http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
         */
        try {
            boolean isWindows = false;
            if (System.getProperty("os.name").startsWith("Windows")) {
                isWindows = true;
            }

            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {

                if ((isWindows && "Windows".equals(info.getName())) || (!isWindows && "Nimbus".equals(info.getName()))) {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | javax.swing.UnsupportedLookAndFeelException ex) {
            java.util.logging.Logger.getLogger(MainFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        //</editor-fold>

        /*
         * Create and display the form
         */
        java.awt.EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                new MainFrame().setVisible(true);
            }
        });
    }
    private static String aboutText = null;
    private Mixer.Info[] mixerInfo = null;
    private Mixer mixer = null;
    private Clip clip = null;
    private Audiometry audiometry;
    private SoundSample soundSample;
    private Timer timerSoundSample;
    private int samplesCount;
    private boolean heardLastSound = false;
    private boolean saved = false;
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton jButtonLeftEar;
    private javax.swing.JButton jButtonPlayTestSound;
    private javax.swing.JButton jButtonReset;
    private javax.swing.JButton jButtonRightEar;
    private javax.swing.JButton jButtonSave;
    private javax.swing.JCheckBox jCheckBoxLeftOn;
    private javax.swing.JCheckBox jCheckBoxRightOn;
    private javax.swing.JCheckBox jCheckBoxShowSoundProperties;
    private javax.swing.JDialog jDialogChooseAudioOutput;
    private javax.swing.JLabel jLabel1;
    private javax.swing.JLabel jLabel2;
    private javax.swing.JLabel jLabel3;
    private javax.swing.JLabel jLabel4;
    private javax.swing.JLabel jLabel5;
    private javax.swing.JLabel jLabel6;
    private javax.swing.JLabel jLabel7;
    private javax.swing.JLabel jLabel8;
    private javax.swing.JLabel jLabel9;
    private javax.swing.JLabel jLabelDialogStatusBar;
    private javax.swing.JLabel jLabelLegend;
    private javax.swing.JList<Line.Info> jListLines;
    private javax.swing.JList<Mixer.Info> jListMixers;
    private javax.swing.JMenuBar jMenuBar1;
    private javax.swing.JMenu jMenuFile;
    private javax.swing.JMenu jMenuHelp;
    private javax.swing.JMenuItem jMenuItemAbout;
    private javax.swing.JMenuItem jMenuItemExit;
    private javax.swing.JMenuItem jMenuItemHelp;
    private javax.swing.JMenuItem jMenuItemSave;
    private javax.swing.JMenuItem jMenuItemSelectAudioOutput;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JPanel jPanel2;
    private javax.swing.JPanel jPanel3;
    private javax.swing.JPanel jPanel4;
    private javax.swing.JPanel jPanel5;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JScrollPane jScrollPane2;
    private javax.swing.JScrollPane jScrollPane3;
    private javax.swing.JScrollPane jScrollPane4;
    private javax.swing.JPopupMenu.Separator jSeparator2;
    private javax.swing.JSlider jSliderMasterLevel;
    private javax.swing.JTable jTableExamresults;
    private javax.swing.JTextArea jTextArea1;
    private javax.swing.JTextField jTextFieldAmplitude;
    private javax.swing.JTextField jTextFieldFrequency;
    private javax.swing.JTextField jTextFieldSoundDuration;
    private javax.swing.JTextField jTextFieldTestSoundDuration;
    private javax.swing.JTextField jTextFieldTimeBetweenSounds;
    private javax.swing.JToggleButton jToggleButtonStartPause;
    // End of variables declaration//GEN-END:variables
}
